/** 
* @file pmu.cpp
* Physical Memory Unit.
* Unit for alocation and management of physical memory.
* Memory is separate to logical blocks (frames). PMU manages
* allocation of this blocks over bitmap array.
*
* Copyrights 2010 Michal Saman, m.saman@designplus.cz.
* This source code is release under the Apache License 2.0.
* http://www.apache.org/licenses/LICENSE-2.0
*/

#include <stdint.h>
#include <string.h>
#include <mem/pmu.h>
#include <main/kprintf.h>
#include <main/boot_info.h>

/** Total size of physical memory. Should be set by kernel at start to real value obtained from BIOS. */
static uint32_t pmu_memory_size = 0;

/** Number of blocks currently in use. */
static uint32_t pmu_used_blocks = 0;

/** Total count of defined memory blocks. */
static uint32_t pmu_max_blocks = 0;

/** Memory map bit array. Each bit of this map represents one memory block */
static uint32_t *pmu_memory_map = 0;

/** Memory region map loaded by bios to 0x1000 */
struct memory_region *m_region = (struct memory_region*)0x1000;

/**
* Set bit in pmu_memory_map.
* @param bitNum Position number of bit that we want to set.
*/
inline void mmap_set(int bitNum) {
	pmu_memory_map[bitNum/32] |= (1<<(bitNum % 32));
}

/** 
* Unset bit in pmu_memory_map (bitmap array).
* @param bitNum Position number of bit that we want to unset.
*/
inline void mmap_unset(int bitNum) {
	pmu_memory_map[bitNum/32] &= ~(1<<(bitNum % 32));
}

/** 
* Test if specified bit is set within the memory map bit array.
* @param bitNum Position of bit that we want to test (if set).
*/
inline bool mmap_test(int bitNum) {
	return pmu_memory_map[bitNum/32] & ~(1<<(bitNum%32));
}

/**
* Find first free block in the bit array and returns its position index.
*/
int mmap_first_free() {
	uint32_t i, j;
	for (i=0; i<pmu_get_block_count()/32; i++)
		if (pmu_memory_map[i] != 0xFFFFFFFF)  // speedup, if whole int is set skip it
			for(j=0; j<32; j++) {
				int bit = 1 << j;
				if (!(pmu_memory_map[i] & bit))
					return i*32+j;
			}
	return -1;
}

/**
* Find first free region of blocks of specified "size" and return its index.
* @param size_t Size (in count of blocks) of free region to find.
*/
int mmap_first_free_s(size_t size) {
	if (size==0)
		return -1;
	if (size==1)
		mmap_first_free();
	uint32_t i, j, count;
	for(i=0; i<pmu_get_block_count()/32; i++)
		if (pmu_memory_map[i] != 0xFFFFFFFF)
			for(j=0; j<32; j++) {
				int bit = 1<<j;
				if (!(pmu_memory_map[i]&bit)) {
					int startBit = i*32;
					startBit+=bit;
					uint32_t free = 0;
					for (count=0; count<=size; count++) {
						if (!mmap_test(startBit+count))
							free++;
						if (free==size)
							return i*32+j;
					}
				}
			}
		return -1;
}

/**
* Inicialize Physical Memory Unit.
* By default set all memory as in use (all blocks set as used).
* @param bitmap  Address of bitmap array used for definition memory map.
*/
void pmu_init(physical_addr bitmap) {
	pmu_detect_phys_memory_size();


	pmu_memory_map = (uint32_t*) bitmap;
	pmu_max_blocks = (pmu_get_memory_size()*1024)/PMU_BLOCK_SIZE;
	kprintf("Memory map block count: %i\n", pmu_max_blocks );
	pmu_used_blocks = pmu_max_blocks;
	// By default, all of memory is in use
	memset(pmu_memory_map, 1, pmu_get_block_count()/PMU_BLOCKS_PER_BYTE);


	// Init avaiable memory regions
	int i;
	for (i=0; i<15; i++) {
		if (i>0 && m_region[i].startLo==0)
			break;
		if (m_region[i].type==1)
			pmu_init_region(m_region[i].startLo, m_region[i].sizeLo);
	}
	kprintf("Memory map size: %ib, address: 0x%x\n", pmu_get_memory_map_size(), bitmap );

}

/**
* Init (set as free) specified size of memory.
* @param size Size of memory to init (in bytes). Size of region to init.
* @param base Base address of memory (region).
*/
void pmu_init_region(physical_addr base, size_t size) {
	// check if region isn't smaller then one block
	if (size<PMU_BLOCK_SIZE) 
		return;
	int align = base / PMU_BLOCK_SIZE;
	int blocks = size / PMU_BLOCK_SIZE;

	for (; blocks>=1; blocks--) {
		mmap_unset(align++);
		pmu_used_blocks--;
	}
	//kprintf("\n used blocks: %i\n", pmu_used_blocks);
	mmap_set(0); // first block is always set. This insures allocs cant be 0
}

/**
* Deinit (set as used) specified size of memory.
* @param size Size of memory to deinit (in bytes).
* @param base Base address of memory.
*/
void pmu_deinit_region(physical_addr base, size_t size) {
	int align = base / PMU_BLOCK_SIZE;
	int blocks = size / PMU_BLOCK_SIZE;
	// if region size is not aligned to block size, we must allocate whole last block
	if (size % PMU_BLOCK_SIZE > 0)
		blocks++;

	for (; blocks>=1; blocks--) {
		mmap_set(align++);
		pmu_used_blocks++;
	}
}

/**
* Allocate one block in memory (set as used in memory map).
* Return address of block.
*/
void *pmu_alloc_block() {
	if (pmu_get_free_block_count()<=0) 
		return 0; // out of mem
	int frame = mmap_first_free();
	if (frame == -1)
		return 0; // out of mem
	mmap_set(frame);
	physical_addr addr = frame * PMU_BLOCK_SIZE;
	pmu_used_blocks++;
	return (void*)addr;
}

/**
* Free specfied block of memory.
* @param p Address of block to free
*/
void pmu_free_block(void* p) {
	physical_addr addr = (physical_addr)p;
	int frame = addr / PMU_BLOCK_SIZE;
	mmap_unset(frame);
	pmu_used_blocks--;
}

/**
* Allocate (set as used) specified count of blocks in memory.
* @param size Count of blocks to allocate
* @returns base address of first block
*/
void *pmu_alloc_blocks(size_t size) {
	if (pmu_get_free_block_count() <= size)
		return 0; // out of mem
	int frame = mmap_first_free_s(size);
	if (frame==1)
		return 0;
	uint32_t i;
	for (i=0; i<size; i++)
		mmap_set(frame+i);
	physical_addr addr = frame * PMU_BLOCK_SIZE;
	pmu_used_blocks+=size;
	return (void*)addr;
}

/**
* Free specified count of blocks in memory.
* @param p address of first block
* @param size Count of blocks to free
*/
void pmu_free_blocks(void* p, size_t size) {
	physical_addr addr = (physical_addr)p;
	int frame = addr / PMU_BLOCK_SIZE;
	uint32_t i;
	for (i=0; i<size; i++)
		mmap_unset(frame+i);

	pmu_used_blocks-=size;
}

/**
* Return total size of physical memory of computer.
*/
size_t pmu_get_memory_size() {
	return pmu_memory_size;
}

/**
* Returns total count of used blocks.
*/
uint32_t pmu_get_use_block_count() {
	return pmu_used_blocks;
}

/**
* Returns total count of free blocks
*/
uint32_t pmu_get_free_block_count() {
	return pmu_max_blocks - pmu_used_blocks;
}

/**
* Returns total count of block
*/
uint32_t pmu_get_block_count() {
	return pmu_max_blocks;
}

/**
* Return siye of block in bytes
*/
uint32_t pmu_get_block_size() {
	return PMU_BLOCK_SIZE;
}

/**
* Return size (in bytes) of binary memory map used for block allocation.
*/
extern size_t pmu_get_memory_map_size() {
	uint32_t size = pmu_max_blocks / 32;
	if (pmu_max_blocks % 32 > 0)
		size++;
	return size*4;
}

/**
* Detect physical memory size from BIOS memory map and boot info structure.
*/
void pmu_detect_phys_memory_size() {
	// memory size obtained from BIOS
	uint32_t mem_size_bios = 1024 + _boot_info->memoryLo + _boot_info->memoryHi*64;
	// memory size calculated from bios map
	uint32_t mem_size_map = 0;

	int i;
	for (i=0; i<15; i++) {
		// if type is larger than 4 then is reserved
		if (m_region[i].type>4)			
			m_region[i].type=1;
		// if start address is 0, there is no more entries, break out
		if (i>0 && m_region[i].startLo==0)
			break;
		// displaying only Low addresses (because we're for 32bit platform)
		//kprintf("[Region %i] start: 0x%x length: %i kB type: %i (%s)\n", i,
		//	m_region[i].startLo,
		//	m_region[i].sizeLo / 1024 ,
		//	m_region[i].type, str_memory_types[m_region[i].type-1]);
		// if region is real memory, use it as size of physical memory
		if (m_region[i].startLo>=0)
			mem_size_map += m_region[i].sizeLo/1024;
	};

	// Set memory size, we trust memory_size_map if avaiable.
	if (mem_size_map>0)
		pmu_memory_size = mem_size_map;
	else
		pmu_memory_size=mem_size_bios;
	kprintf("Detected size of physical memory: %i kB (map: %i, bios: %i)\n", pmu_get_memory_size(), mem_size_map, mem_size_bios);
}
